<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>bind</title>
</head>
<body>
    <script>
        // 示例 1:
        const goods = {
            name: 'orange',
            getName: function() {
                return this.name
            }
        }
        const unBindGetName = goods.getName;
        console.log(unBindGetName());  // this 指向的是 window，所有打印结果是 ''
        const bindGetName = goods.getName.bind(goods);
        console.log('bindGetName :>> ', bindGetName);   //  getName: function() { return this.name }
        console.log('bindGetName :>> ', bindGetName()); // bindGetName :>>  orange


        // 示例 2:
        function Point(x, y) {
            this.x = x
            this.y = y
        }
        Point.prototype.getPos = function() {
            return `${this.x}, ${this.y}`
        }
        const p = new Point(1, 2)
        console.log('p.getPos :>> ', p.getPos()) // p.getPos :>>  1, 2
        const obj = { x: 7, y: 8 }
        const BindPoint = Point.bind(obj, 3, 4)
        const bindP = new BindPoint(5, 6)
        console.log('BindPoint :>> ', BindPoint); // ƒ Point(x, y) { this.x = x this.y = y }
        console.log('Point :>> ', Point); // ƒ Point(x, y) { this.x = x this.y = y }
        console.log('bindP.getPos :>> ', bindP.getPos()) // bindP.getPos :>>  3, 4
        console.log('bindP instanceof Point :>> ', bindP instanceof Point);  // bindP instanceof Point :>>  true
        console.log('bindP instanceof BindPoint :>> ', bindP instanceof BindPoint); // bindP instanceof BindPoint :>>  true
        console.log('BindPoint.prototype :>> ', BindPoint.prototype); // BindPoint.prototype :>>  undefined
        console.log('Point.prototype :>> ', Point.prototype); // constructor: function Point(x, y) {}


        /**
         * bind 源码分析
         * bind 方法创建一个新的函数
         * 在 bind() 函数被调用时
         * 新函数的 this 会被指定为 bind() 的第一个参数 
         * 其余参数将作为新函数的参数
         */
        // 第一种实现方式
        ;(function(window) {
            if(!Function.prototype._bind) {
                Function.prototype._bind = function() {
                    let fn = this,  // 谁调用 _bind 谁就是 this，也就是原始函数
                        slice = Array.prototype.slice,
                        context = arguments[0], // 取到 bind 函数的第一个入参，也就是要绑定的上下文环境
                        args = slice.call(arguments, 1) // 截取参数，供函数调用时使用

                    if(typeof fn !== 'function') {  // 如果不是函数，则提示
                        throw new TypeError('必须是函数才能绑定')
                    }
                    return function() {
                        console.log('this :>> ', this); // window
                        const bindArgs = slice.call(arguments)  // bind 后的函数，传入的入参
                        const allArgs = args.concat(bindArgs)   // 拼接 bind 的入参和 bind 之后的函数的入参
                        return fn.apply(context, allArgs)   // 执行函数，使用 apply 改变函数的执行者
                    }
                }
            }
        })(window)

        // 测试
        const testObj = {
            language: 'JavaScript',
            getLang: function() {
                return this.language
            }
        }
        const unBindGetLang = testObj.getLang
        const bindGetLang = unBindGetLang._bind(testObj)
        console.log('bindGetLang() :>> ', bindGetLang()) // bindGetLang() :>>  JavaScript


        // 第二种实现方式
        ;(function(window) {
            if(!Function.prototype.myBind) {
                Function.prototype.myBind = function() {
                    let fn = this,                              // 谁执行 bind，this 就指向谁，this此时指向的原函数
                        slice = Array.prototype.slice,          // 缓存数组的 slice 方法
                        context = arguments[0],                 // 获取 bind() 函数的第一个参数，也就是要绑定的上下文环境
                        args = slice.call(arguments, 1),        // 获取 bind() 函数的入参
                        fNOP = function() {},                   // 定义一个空函数
                        fnBind = function() {
                            const bindArgs = slice.call(arguments)  // 获取入参，转为数组
                            const allArgs = args.concat(bindArgs)   // 将 bind() 函数的入参与返回函数的入参拼接起来

                            console.log(this);  // fnBind {}   this 指向 fnBind 构造函数的实例  或者指向 Window
                            /**
                             * 此处需要判断是不是 new 操作符构造出来的实例
                             * 1.如果是 new 操作符执行的，则 this 指向的是 new 出来的实例，即 fnBind {} 
                             * 2.否则，this 指向 window 对象
                            */

                            // 第一种判断方法, __proto__ 在IE下存在兼容性问题，不推荐使用
                            const isUseNew1 = this.__proto__ === fnBind.prototype;
                            console.log('isUseNew1 :>> ', isUseNew1);     
                            
                            // 第二种判断方法
                            const isUseNew2 = this instanceof fNOP
                            console.log('isUseNew2 :>> ', isUseNew2);

                            // 第三种判断方法
                            const isUseNew3 = fNOP.prototype.isPrototypeOf(this)
                            console.log('isUseNew3 :>> ', isUseNew3);
                            
                            const fnContext = isUseNew3 ? this : context
                            return fn.apply(fnContext, allArgs)
                        }

                    if(typeof fn !== 'function') {
                        throw new TypeError('bind 绑定的必须是函数')
                    }

                    // 如果原函数有 prototype 属性，就将原函数的 prototype 赋值给 fNOP的原型
                    // fNOP 和 原函数原型就指向同一块内存空间
                    if(fn.prototype) {
                        fNOP.prototype = fn.prototype
                    }

                    // fnBind 的原型指向 fNOP的实例，通过原型链实现了一个继承
                    fnBind.prototype = new fNOP()
                    // fnBind.prototype = Object.create(fNOP.prototype) // 包装对象
                    return fnBind
                }
            }
        })(window)
        // test 
        function Person(name, age) {
            this.name = name
            this.age = age
        }
        Person.prototype.getInfo = function() {
            return `${this.name}, ${this.age}`
        }
        const personObj = { name: 'Jack', age: 18 }
        const BindPerson = Person.myBind(personObj, 'Alen', 20)  // bindPerson.getInfo :>>  Alen, 20
        const bindPerson = new BindPerson()
        console.log('bindPerson.getInfo :>> ', bindPerson.getInfo()); 
        console.log('bindPerson :>> ', bindPerson); // bindPerson :>>  fnBind {name: "Alen", age: 20}

        // test    
        function getX() {
            return this.x
        }
        const obj2 = {
            x: 2
        }
        const bindGetX = getX.myBind(obj2)
        console.log('bindGetX() :>> ', bindGetX())  // bindGetX() :>>  2

    </script>
</body>
</html>